package org.hugoduncan.appam.component;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URISyntaxException;

import javax.activation.MimeTypeParseException;

import org.apache.abdera.factory.Factory;
import org.apache.abdera.model.Document;
import org.apache.abdera.model.Entry;
import org.apache.abdera.model.ExtensibleElement;
import org.apache.abdera.model.Feed;
import org.apache.abdera.model.Link;
import org.apache.abdera.parser.ParseException;
import org.apache.abdera.parser.Parser;
import org.apache.abdera.xpath.XPath;
import org.apache.abdera.xpath.XPathException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hugoduncan.appam.model.CollectionComponent;
import org.hugoduncan.appam.model.Constants;
import org.hugoduncan.appam.server.AppServletRequest;
import org.hugoduncan.appam.server.AppServletResponse;

/** A collection that stores its feed in the filesystem.
 */
public class FileSystemCollection extends BaseCollection
    implements CollectionComponent
{
    static final Log log = LogFactory.getLog(FileSystemCollection.class);

    /**
     * Feed document, describing the feed.
     */
    File feedFile;

    @Override
    protected synchronized void sendFeed(AppServletRequest request,
                                         AppServletResponse response)
        throws IOException
    {
        Feed feedElement=null;

        long ifModifiedSince=request.getDateHeader("If-Modified-Since");
        if (ifModifiedSince>0 && feedFile.exists())
        {
            if (ifModifiedSince<feedFile.lastModified())
            {
                response.setStatus(AppServletResponse.SC_NOT_MODIFIED);
                return;
            }
        }

        if (!feedFile.exists())
        {
            feedElement=createFeed(response.getBaseUrl());
            saveFeed(feedElement);
        }

        if (log.isDebugEnabled())
        {
            StringWriter sw=new StringWriter();
            try {
                FileInputStream is=new FileInputStream(feedFile);
                IOUtils.copy(is, sw);
                is.close();
            } catch (IOException e) {
                log.error("Coldn't produce debug output for feed");
            }
            log.debug("Introspection document sent");
            log.debug(sw.toString());
        }

        response.setCharacterEncoding("utf-8");
        response.setContentType(Constants.SC_ATOM_CONTENT_TYPE);
        response.setContentLength((int)feedFile.length());

        FileInputStream is=new FileInputStream(feedFile);
        IOUtils.copy(is, response.getOutputStream());
        response.flushBuffer();
        is.close();
    }

    
    @Override
    protected synchronized void updateFeed(
        AppServletRequest request, AppServletResponse response,
        Entry entry)
        throws IOException
    {
        log.debug("Updating feed to reflect entry following entry: ");
        debugWrite(entry);

        Feed feedElement=getFeedElement(response.getBaseUrl());

        if (request.getMethod().equals("POST")
            || request.getMethod().equals("PUT"))
        {
            removeEntryFromFeedIfExists(entry, feedElement);
            feedElement.addEntry(entry);
            saveFeed(feedElement);
        }
        else if (request.getMethod().equals("DELETE"))
        {
            removeEntryFromFeedIfExists(entry, feedElement);
            saveFeed(feedElement);
        }
    }

    private void removeEntryFromFeedIfExists(Entry entry, Feed feedElement)
    {
        try {
            // Default XPath instance has prefix a for atom namespace
            Entry existingEntry = (Entry)XPath.INSTANCE.selectSingleNode(
                    "a:entry[a:id=\""+entry.getId().toString()+"\"]",
                    feedElement);
            if (existingEntry!=null)
                existingEntry.discard();
        } catch (XPathException e) {
            log.error(e);
        } catch (URISyntaxException e) {
            log.error(e);
        }
    }


    protected Feed createFeed(String baseUrl) {
        Factory factory=Factory.INSTANCE;
        Feed feed=factory.newFeed();
        feed.setTitle(collection.getTitle());
        try {
            feed.setBaseUri(baseUrl);
            Link link=feed.addLink(location);
            link.setMimeType(Constants.SC_ATOM_CONTENT_TYPE);
            link.setTitle(collection.getTitle());
        } catch (URISyntaxException e) {
            log.error("Unexpected exception:",e);
        } catch (MimeTypeParseException e) {
            log.error("Unexpected exception:",e);
        }
        return feed;
    }

    private Feed getFeedElement(String baseUrl) throws IOException
    {
        if (!feedFile.exists())
        {
            Feed feedElement=createFeed(baseUrl);
            saveFeed(feedElement);
            return feedElement;
        }
        try {
            FileInputStream is=new FileInputStream(feedFile);
            Document<Feed> document=Parser.INSTANCE.parse(is);
            is.close();
            return document.getRoot();
        }
        catch (ParseException e) {
            log.error("Could not parse feed",e);
            throw(e);
        }
    }

    private void saveFeed(Feed feedElement) throws IOException
    {
        File tmp=File.createTempFile("atom", ".atom");
        FileOutputStream os=new FileOutputStream(tmp);
        feedElement.getDocument().writeTo(os);
        os.flush();
        os.close();

        if (!feedFile.exists() || feedFile.delete())
            tmp.renameTo(feedFile);
        else
            log.error("Could not delete existing feed file to rewrite it");
    }

    private <T extends ExtensibleElement> void debugWrite(T element)
    {
        try {
            StringWriter sw=new StringWriter();
            element.writeTo(sw);
            log.debug(sw.toString());
        } catch (IOException e1) {
            log.error("Could not output document",e1);
        }
    }

    public void setFeed(File feed) {
        this.feedFile = feed;
    }
}
